<?php
/**
 * Parcl
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://parcl.com/license
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@parcl.com so we can send you a copy immediately.
 *
 * @category parcl
 * @package parcl-core
 * @subpackage debug
 * @copyright Copyright (c) 2006-2011 Mikael Knutsson (http://mikn.me)
 * @license http://parcl.com/license New BSD License
 */

if(PARCL_EXEC !== 1) {
	die('<h1>Incorrect Access</h1>You cannot access this file directly.<br/>' .
		'If you got this error message by clicking a link on the site,' .
		'please notify the webmaster as soon as possible.');
}

/**
 * Defines the different log severity levels available.
 * 0-2 defines levels from which the application cannot recover.
 * 3 defines errors from which recovery might sometimes be possible.
 * 4-5 defines errors from which recovery is possible, but investigation might
 * be required.
 * 6 is for informational messages, for example, where in the execution the
 * application is.
 * 7 is for debugging only. This enables a lot of spam.
 * @abstract
 */

abstract class LogLevel {
	/**
	 * @var int System is unusable.
	 */
	const EMERGENCY = 0;
	/**
	 * @var int Action must be taken immediately.
	 */
	const ALERT = 1;
	/**
	 * @var int Critical conditions.
	 */
	const CRITICAL = 2;
	/**
	 * @var int Error conditions.
	 */
	const ERROR = 3;
	/**
	 * @var int Warning conditions.
	 */
	const WARNING = 4;
	/**
	 * @var int Normal but significant conditions.
	 */
	const NOTICE = 5;
	/**
	 * @var int Informational messages.
	 */
	const INFORMATIONAL = 6;
	/**
	 * @var int Debug-level messages.
	 */
	const DEBUG = 7;
}

/**
 * Purpose	 : To allow portable and customisable debugging functions.
 * Description : The debug class was introduced to not cram the wrapper class
				 with stuff, and to allow for a more extensible system for
				 debugging.
				 Debugging levels:
				 0 = No debug.
				 1 = Full debug.
				 2 = Balanced debug.
 * - Construct -
 * Input	   : An integer corresponding to a specific debugging level which
				 in turn decides to what extents debugging functions are executed.
 * Description : The construct of the debug class, it triggers the right methods
				 depending on the debug level passed to it.
 * Errors	  : I set the $level function argument as optional to be able to throw
				 custom errors if it isn't set. It is to be able to deal with it
				 transparently in a production enviroment.
				 The class instantiation fails if $level isn't explicitly an integer.
 * Dev Notes   : Need to come up with a more dynamic classification system for
				 debugging and calling of appropriate methods to execute them.
 */

class watchdog extends Events {
	private static $debugSeverity = array(4, 7, 6);
	private static $level = null;
	/**
	 * @var array The static cache for log entries. It will be emptied by the
	 * attached log method.
	 */
	private static $logArray = array();

	public function __construct($level = null) {
		if(!is_int($level)) {
			throw new GeneralException('Unauthorized call of the Debug class',
									GeneralException::UNAUTHACC);
		}
		$registry = Registry::GetInstance();
		$registry->Register('watchdog', $this);
		if($level > 0) {
			error_reporting(E_NOTICE | E_ALL);
			$startTime = Timers::StartTimer();
			ob_start();

			$registry->Register('debugStartTime', $startTime);
			$registry->Register('debugInitModules', new ArrayObject());
		}
		self::$level = $level;
	}

	/**
	 * Standard logging method. Leaving the variables out of the logged string
	 * for i18n possibilities.
	 * @param string $message The message being logged.
	 * @param int $severity Severity of the message.
	 * @param array $variables The variables in the string.
	 * @uses watchdog::$logArray Temporary cache, should no log writer be attached.
	 * @uses EventHandlerCollection Accesses the collection directly to allow
	 * the method to be static.
	 * @static
	 */
	public static function Log($message, $severity, $variables = array()) {
		if($severity > self::$debugSeverity[self::$level]) {
			return;
		}
		if(!is_int($severity)) {
			throw new GeneralException('Log message severity is not an integer. Value: ' . print_r($severity, true), GeneralException::VAR_TYPE_NOTICE);
		}
		if(!is_string($message)) {
			throw new GeneralException('Message is not a string. Value: ' . print_r($severity, true), GeneralException::VAR_TYPE_NOTICE);
		}
		if(!is_array($variables)) {
			throw new GeneralException('Variables is not an array. Value: ' . print_r($variables, true), GeneralException::VAR_TYPE_NOTICE);
		}
		$eventHandlers = EventHandlerCollection::GetInstance();
		$registry = Registry::GetInstance();

		$logEntry = array('time' => microtime(), 'message' => $message, 'severity' => $severity, 'variables' => $variables);
		self::$logArray[] = $logEntry;
		$eventHandlers->RaiseEvent('L:MESSAGE_RECEIVED', $registry->Get('watchdog'), array('logEntries' => self::$logArray));
	}

	public static function EmptyLogCache() {
		self::$logArray = array();
	}

	/**
	 * Input	   : $sender: The $this variable of the calling class. This variable is
					 currently unused, but all event handlers need to accept it.
					 $args: An array of arguments sent from the calling class.
	 * Output	  : None.
	 * Description : This function is an event handler and a wrapper for the debug-class'
					 event handlers added to different events since the debug class
					 is instanciated earlier than the wrapper class.
	 * Errors	  : No error conditions have been defined.
	 * Assumptions : Assumes that the classes EventHandlerCollection, EventHandler and Event
					 are available.
	 */

	public function EventRegistar($sender, $args) {
		$eventHandlers = EventHandlerCollection::GetInstance();
		$eventHandlers->Add(new EventHandler(new Event('L:MESSAGE_RECEIVED'),
											 array('FileLogging', 'OnMessageReceived')));
		if(self::$level > 0) {
			$eventHandlers->Add(new EventHandler(new Event('P:PAGE_PREPARED'),
												   array('watchdog', 'PagePrepared')));
			$eventHandlers->Add(new EventHandler(new Event('P:MODULE_LOADING'),
												   array('watchdog', 'ModuleLoading')));
			$eventHandlers->Add(new EventHandler(new Event('DB:QUERY_RECEIVED'),
												   array('parclDB', 'qStartTimer')));
			$eventHandlers->Add(new EventHandler(new Event('DB:EXECUTION_MADE'),
												   array('parclDB', 'qEndTimer')));
		}
	}

	public function ModuleLoading(Modules $sender, $args) {
		$className = $args['className'];
		$registry = Registry::GetInstance();
		$initModules = $registry->Get('debugInitModules');
		$initModules->Append($className);
	}

	/**
	 * Input	   : $sender: The $this variable of the calling class.
					 $content: The output generated by parcl::getContent().
	 * Output	  : None.
	 * Description : This is a test function that adds content to the page after
					 the content has been generated.
	 * Errors	  : No error conditions have been defined.
	 * Assumptions : That $content is the correct input.
	 */

	public function PagePrepared($sender, $args) {
		$events = EventCollection::GetInstance();
		$registry = Registry::GetInstance();
		$content = &$args['pageData'];
		$extraTemplates = &$args['extraTemplates'];
		$debugData = null;
		$errors = ob_get_contents();
		ob_end_clean();

		$content['content']['debug']['title'] = 'Debug data';
		if(strlen($errors) > 0) {
			$debugData .= "PHP Errors and Notices: \n";
			$debugData .= $errors;
		}

		$debugData .= 'Modules in ' . MODULE_DIR . ': ';
		foreach($registry->Get('modules') as $module) {
			$debugData .= $module . ', ';
		}
		$debugData .= 'The modules of those with "queries" folders: ';
		foreach($registry->Get('QueryModules') as $module) {
			$debugData .= $module . ', ';
		}
		$debugData .= 'These modules have been initiated: ';
		foreach($registry->Get('debugInitModules') as $module) {
			$debugData .= $module . ', ';
		}
		$debugData .= 'This much time was spent doing queries: ' . round(parclDB::getQueryTime(), 5);
		$debugData .= ' These events were triggered: ';
		foreach($events->GetFlagTriggered(false) as $event) {
			$debugData .= $event->GetName() . ', ';
		}
		//$debugData .= print_r($_ENV['TZ'], true);

		$stopTime = round(Timers::StopTimer($registry->Get('debugStartTime')), 5);
		$dbTime = round(parclDB::getQueryTime() / $stopTime * 100, 2);
		$params = array('pageTime' => (string) $stopTime . ' secs ' ,
						'queryTime' => (string) $dbTime . '% time spent on ',
						'queryCount' => (string) parclDB::getQueryCount() . ' queries',
						);
		$extraTemplates[] = 'debug';
		$content['content']['debug']['content'] = $debugData;
		$content['parcels']['footer']['debug'] = $params;
	}
}

/**
 * Very crude logging class.
 */
class FileLogging implements ILogging {
	private static $file = 'parcl.log';
	private static $firstTime = null;
	public function __construct() {}
	
	public static function OnMessageReceived($sender, $args) {
		$file = ROOT_DIR . self::$file;
		$logEntries = $args['logEntries'];
		if(is_file($file) && !is_writable($file)) {
			echo "Can't write to file!" . " Wanted to write: " . var_dump($logEntries);
			return;
		}
		if(!is_file($file)) {
			$handle = fopen($file, 'x');
		}
		else {
			$handle = fopen($file, 'a');
		}
		foreach($logEntries as $logEntry) {
			list($ms, $time) = explode(' ', $logEntry['time']);
			$date = sprintf('%s.%s', date('d-M H:i:s', $time), substr($ms, 2));
			$msTime = (float)$time + (float)$ms;
			if(is_null(self::$firstTime)) {
				$entry = sprintf("\r\nCall at: %s\r\n", $date);
				self::$firstTime = $msTime;
				fwrite($handle, $entry);
			}
			$entry = sprintf("%f | %d - %s\r\n", (float)$msTime - self::$firstTime, $logEntry['severity'], strtr($logEntry['message'], $logEntry['variables']));
			fwrite($handle, $entry);
		}
		fclose($handle);
		watchdog::EmptyLogCache();
	}
}

abstract class Timers {

	/**
	 * Input	   : None.
	 * Output	  : It returns a float Unix-time of the time with micro seconds
					 of when the function was called upon.
	 * Description : The function is made to time page creation time.
					 The "get_as_float" operator was added to microtime() in PHP5.0.
	 * Errors	  : Unable to fail?
	 */
	public static function StartTimer() {
		$startTime = microtime(true);
		return $startTime;
	}

	/**
	 * Input	   : $startTime: A start time as a Unix time float with microseconds.
	 * Output	  : The difference between $startTime and when this function was called upon.
	 * Description : The function is made to time page creation time. It calculates
					 the difference between $startTime and when this function was called upon.
	 * Errors	  : Throws a VAR_TYPE exception if $startTime is not float.
	 * Assumptions : None.
	 */

	public static function StopTimer($startTime) {
		if(! is_float($startTime)) {
			throw new GeneralException(sprintf('Start time was not a float, it was of type %s and had value %s.',
								   gettype($startTime), print_r($startTime, true)),
								   GeneralException::VAR_TYPE);
		}
		$endTime = microtime(true);
		$totalTime = $endTime - $startTime;
		return $totalTime;
	}
}

abstract class StringTools {
	/**
	 * An unforgiving way to force strings to conform to a certain pattern.
	 * @param string $entry String that needs to be conformed.
	 * @param int $minLength How long the string should at least be.
	 * @param int $maxLength How long the string can be at most.
	 * @return string The string after being conformed.
	 */
    public static function MoldEntry($entry, $minLength, $maxLength) {
    	if(preg_match('/^[0-9a-z _.åäö]{' . "$minLength,$maxLength" . '}$/i', $entry)) {
    		return $entry;
    	}
    	return null;
	}
}

abstract class Crypto {
	/**
	*
	* @version Version 0.1 / slightly modified for phpBB 3.0.x (using $H$ as hash type identifier)
	*
	* Portable PHP password hashing framework.
	*
	* Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
	* the public domain.
	*
	* There's absolutely no warranty.
	*
	* The homepage URL for this framework is:
	*
	*	http://www.openwall.com/phpass/
	*
	* Please be sure to update the Version line if you edit this file in any way.
	* It is suggested that you leave the main version number intact, but indicate
	* your project name (after the slash) and add your own revision information.
	*
	* Please do not change the "private" password hashing method implemented in
	* here, thereby making your hashes incompatible.  However, if you must, please
	* change the hash type identifier (the "$P$") to something different.
	*
	* Obviously, since this code is in the public domain, the above are not
	* requirements (there can be none), but merely suggestions.
	*
	*
	* Hash the password
	*/
	public static function Hash($password) {
		$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

		$random_state = unique_id();
		$random = '';
		$count = 6;

		if(($fh = @fopen('/dev/urandom', 'rb'))) {
			$random = fread($fh, $count);
			fclose($fh);
		}

		if(strlen($random) < $count) {
			$random = '';

			for($i = 0; $i < $count; $i += 16) {
				$random_state = md5(unique_id() . $random_state);
				$random .= pack('H*', md5($random_state));
			}
			$random = substr($random, 0, $count);
		}

		$hash = self::hash_crypt_private($password, self::hash_gensalt_private($random, $itoa64), $itoa64);

		if(strlen($hash) == 34) {
			return $hash;
		}

		return md5($password);
	}

	/**
	* Check for correct password
	*
	* @param string $password The password in plain text
	* @param string $hash The stored password hash
	*
	* @return bool Returns true if the password is correct, false if not.
	*/
	public static function CheckHash($password, $hash) {
		$itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
		if(strlen($hash) == 34) {
			return(self::hash_crypt_private($password, $hash, $itoa64) === $hash) ? true : false;
		}

		return(md5($password) === $hash) ? true : false;
	}

	/**
	* Generate salt for hash generation
	*/
	private static function hash_gensalt_private($input, &$itoa64, $iteration_count_log2 = 6) {
		if($iteration_count_log2 < 4 || $iteration_count_log2 > 31) {
			$iteration_count_log2 = 8;
		}

		$output = '$H$';
		$output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= 5) ? 5 : 3), 30)];
		$output .= self::_hash_encode64($input, 6, $itoa64);

		return $output;
	}

	/**
	* Encode hash
	*/
	private static function hash_encode64($input, $count, &$itoa64) {
		$output = '';
		$i = 0;

		do {
			$value = ord($input[$i++]);
			$output .= $itoa64[$value & 0x3f];

			if($i < $count) {
				$value |= ord($input[$i]) << 8;
			}

			$output .= $itoa64[($value >> 6) & 0x3f];

			if($i++ >= $count) {
				break;
			}

			if($i < $count) {
				$value |= ord($input[$i]) << 16;
			}

			$output .= $itoa64[($value >> 12) & 0x3f];

			if($i++ >= $count) {
				break;
			}

			$output .= $itoa64[($value >> 18) & 0x3f];
		}
		while($i < $count);

		return $output;
	}

	/**
	* The crypt function/replacement
	*/
	private static function hash_crypt_private($password, $setting, &$itoa64) {
		$output = '*';

		// Check for correct hash
		if(substr($setting, 0, 3) != '$H$') {
			return $output;
		}

		$count_log2 = strpos($itoa64, $setting[3]);

		if($count_log2 < 7 || $count_log2 > 30) {
			return $output;
		}

		$count = 1 << $count_log2;
		$salt = substr($setting, 4, 8);

		if(strlen($salt) != 8) {
			return $output;
		}

		/**
		* We're kind of forced to use MD5 here since it's the only
		* cryptographic primitive available in all versions of PHP
		* currently in use.  To implement our own low-level crypto
		* in PHP would result in much worse performance and
		* consequently in lower iteration counts and hashes that are
		* quicker to crack (by non-PHP code).
		*/
		if(PHP_VERSION >= 5) {
			$hash = md5($salt . $password, true);
			do {
				$hash = md5($hash . $password, true);
			}
			while(--$count);
		}
		else {
			$hash = pack('H*', md5($salt . $password));
			do {
				$hash = pack('H*', md5($hash . $password));
			}
			while(--$count);
		}

		$output = substr($setting, 0, 12);
		$output .= self::_hash_encode64($hash, 16, $itoa64);

		return $output;
	}

}